﻿using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;

namespace NetworkServices
{
    public delegate void FinishDelegate(string response, HttpWebResponse respObj, string searchCookieParam, object userState);

    public class Context
    {
        public HttpWebRequest req;
        public byte[] data;
        public FinishDelegate finishCB;
        public int numRetries;
        public string phpName;
        public string cookie;
        public string postOrGetData;
        public object userState;
        public HttpWebResponse response;
        public byte[] bytesBuffer;
        public StringBuilder aggregatedResponse;
        public Timer ActionTimer;
        public string Sid;
        public bool IsPost;
        public string Referrer;
        public string SearchCookieParam;
        public ManualResetEvent StopEvent;
        public bool shouldEncodeToASCII;

        public void CallFinishCB(string o, HttpWebResponse respObj, string searchCookieParam)
        {
            DisposeTimer();
            finishCB(o, respObj, searchCookieParam, userState);

            lock (AsyncWebRequest.CounterLock)
            {
                AsyncWebRequest.PendingRequests--;
                //Console.WriteLine(phpName + "-");
            }
        }

        public void DisposeTimer()
        {
            if (ActionTimer != null)
            {
                ActionTimer.Dispose();
                ActionTimer = null;
            }
        }
    }

    public class AsyncWebRequest
    {
        public static object CounterLock = new object();
        public static int PendingRequests = 0;

        private ManualResetEvent ShouldStopEvent;
        protected bool ShouldStop()
        {
            if (ShouldStopEvent == null)
                return false;

            return ShouldStopEvent.WaitOne(1, false);
        }

        public HttpWebRequest SendWebBBRequest(string phpName, 
                                               string cookie, 
                                               string postOrGetData, 
                                               int numRetries, 
                                               string sid, 
                                               bool isPost, 
                                               string referrer, 
                                               ManualResetEvent stopEvent, 
                                               int rangeTo,
                                               string creds,
                                               bool shouldEncodeToASCII,
                                               FinishDelegate finishCB, object userState)
        {
            lock (CounterLock)
            {
                PendingRequests++;
                //Console.WriteLine(phpName + "+");
            }

            ShouldStopEvent = stopEvent;

            string sidString = "";
            if (!string.IsNullOrEmpty(sid))
                sidString = "?sid=" + sid;
            
            ServicePointManager.Expect100Continue = false;
            ASCIIEncoding encoding = new ASCIIEncoding();
            byte[] data = encoding.GetBytes(postOrGetData);

            HttpWebRequest req;
            if (isPost)
            {
                req = (HttpWebRequest)WebRequest.Create(phpName + sidString);
            }
            else
            {
                string uri = phpName + (string.IsNullOrEmpty(postOrGetData) ? "" : ("?" + postOrGetData));
                req = (HttpWebRequest)WebRequest.Create(uri);
            }

            req.Timeout = 120000;
            if (isPost) req.ContentLength = data.Length;
            req.ContentType = "application/x-www-form-urlencoded";
            req.Method = isPost ? "Post" : "Get";
            if (!string.IsNullOrEmpty(cookie))
                req.Headers.Add("Cookie: " + cookie);
            req.Accept =
                "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-ms-application, application/vnd.ms-xpsdocument, application/xaml+xml, application/x-ms-xbap, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-silverlight, application/x-shockwave-flash, */*";

            if (isPost)
            {
                req.Referer = referrer;// LoginURL;
            }
            else
            {
                req.Referer = referrer /*SearchURL */ + ";" + sidString;
            }

            if (rangeTo > 0)
                req.AddRange(0, rangeTo);

            if (!string.IsNullOrEmpty(creds))
            {
                // user:password in base64 encoding
                string encodedCreds = Utils.Utils.EncodeTo64(creds);
                req.Headers["Authorization"] = "Basic " + encodedCreds;
            }
            
            Context ctx = new Context();
            ctx.req = req;
            ctx.data = data;
            ctx.finishCB = finishCB;
            ctx.numRetries = numRetries;
            ctx.phpName = phpName;
            ctx.cookie = cookie;
            ctx.postOrGetData = postOrGetData;
            ctx.userState = userState;
            ctx.bytesBuffer = new byte[4096];
            ctx.aggregatedResponse = new StringBuilder();
            ctx.ActionTimer = new Timer(RequestTimeoutCB, ctx, 120000, Timeout.Infinite);
            ctx.Sid = sid;
            ctx.IsPost = isPost;
            ctx.Referrer = referrer;
            ctx.StopEvent = stopEvent;
            ctx.shouldEncodeToASCII = shouldEncodeToASCII;

            if (isPost)
            {
                req.BeginGetRequestStream(GetRequestStreamCB, ctx);
            }
            else
            {
                req.BeginGetResponse(GetResponseCB, ctx);
            }

            return req;
        }

        private void RequestTimeoutCB(object state)
        {
            Context ctx = state as Context;
            ctx.DisposeTimer();
            ctx.req.Abort();
        }

        private void GetRequestStreamCB(IAsyncResult ar)
        {
            Stream newStream;
            Context ctx = ar.AsyncState as Context;

            try
            {
                newStream = ctx.req.EndGetRequestStream(ar);
            }
            catch (Exception)
            {
                if (ShouldStop())
                {
                    ctx.CallFinishCB(null, null, null);
                    return;
                }

                ctx.DisposeTimer();
                ctx.numRetries--;
                if (ctx.numRetries > 0)
                {
                    lock (AsyncWebRequest.CounterLock)
                    {
                        PendingRequests--;
                        //Console.WriteLine(ctx.phpName + "-");
                    }
                    Thread.Sleep(3000);
                    SendWebBBRequest(ctx.phpName, ctx.cookie, ctx.postOrGetData, ctx.numRetries, ctx.Sid, ctx.IsPost, ctx.Referrer, ctx.StopEvent, 0, "", ctx.shouldEncodeToASCII, ctx.finishCB, ctx.userState);
                }
                else
                {
                    ctx.CallFinishCB(null, null, null);
                    return;
                }
                return;
            }

            // Send the data.
            newStream.Write(ctx.data, 0, ctx.data.Length);
            newStream.Close();

            ctx.req.BeginGetResponse(GetResponseCB, ctx);
        }

        private void GetResponseCB(IAsyncResult ar)
        {
            Context ctx = ar.AsyncState as Context;
            HttpWebRequest req = ctx.req;
            ctx.response = null;

            try
            {
                ctx.response = (HttpWebResponse)req.EndGetResponse(ar);
                if (ctx.response.Headers.Get("Set-Cookie") != null)
                    ctx.SearchCookieParam = ctx.response.Headers.Get("Set-Cookie");
            }
            catch (Exception)
            {
                if (ShouldStop())
                {
                    ctx.CallFinishCB(null, null, null);
                    return;
                }

                ctx.DisposeTimer();
                ctx.numRetries--;
                if (ctx.numRetries > 0)
                {
                    lock (CounterLock)
                    {
                        PendingRequests--;
                        //Console.WriteLine(ctx.phpName + "-");
                    }
                    Thread.Sleep(3000);
                    SendWebBBRequest(ctx.phpName, ctx.cookie, ctx.postOrGetData, ctx.numRetries, ctx.Sid, ctx.IsPost, ctx.Referrer, ctx.StopEvent, 0, "", ctx.shouldEncodeToASCII, ctx.finishCB, ctx.userState);
                    return;
                }
                else
                {
                    ctx.CallFinishCB(null, null, null);
                    return;
                }
            }

            Stream respStream = ctx.response.GetResponseStream();
            respStream.BeginRead(ctx.bytesBuffer, 0, 4096, ReadStreamCB, ctx);
        }

        private void ReadStreamCB(IAsyncResult ar)
        {
            int numBytes = 0;
            Context ctx = ar.AsyncState as Context;
            try
            {
                numBytes = ctx.response.GetResponseStream().EndRead(ar);
                if (numBytes > 0)
                {
                    if (ctx.shouldEncodeToASCII)
                    {
                        ctx.aggregatedResponse.Append(Encoding.Default.GetString(ctx.bytesBuffer, 0, numBytes));
                    }
                    else
                        ctx.aggregatedResponse.Append(Encoding.UTF8.GetString(ctx.bytesBuffer, 0, numBytes));

                    ctx.response.GetResponseStream().BeginRead(ctx.bytesBuffer, 0, 4096, ReadStreamCB, ctx);
                }
                else
                {
                    ctx.CallFinishCB(ctx.aggregatedResponse.ToString(), ctx.response, ctx.SearchCookieParam);
                }
            }
            catch (Exception)
            {
                ctx.CallFinishCB(null, null, null);
                return;
            }
        }
    }
}
